package com.nononsenseapps.feeder.parser

import com.nononsenseapps.feeder.net.NetManager
import com.nononsenseapps.jsonfeed.AbstractJsonParser
import com.nononsenseapps.jsonfeed.DEFAULT_CONNECT_TIMEOUT_SECS
import com.rometools.rome.io.ParsingFeedException
import com.rometools.rome.io.XmlReader
import okhttp3.Response
import okio.Buffer
import okio.GzipSource
import timber.log.Timber
import java.io.EOFException
import java.io.File
import java.io.InputStream
import java.net.URL
import java.nio.charset.Charset

val slashPattern = """<\s*slash:comments\s*/>""".toRegex(RegexOption.IGNORE_CASE)
val errorXmlCharPattern="""[\x00-\x08\x0b-\x0c\x0e-\x1f]""".toRegex()

abstract class AbstractObjectParser<T>(@Volatile protected var jsonParser: AbstractJsonParser<T>,protected val netManager: NetManager = NetManager(jsonParser.getHttpClient())) {

    /**
     * To enable caching, you need to call this method explicitly with a suitable cache directory.
     */
    abstract fun setup(cacheDir: File?): AbstractObjectParser<T>

    @Throws(ObjectParsingError::class)
    suspend fun parseObjectUrl(url: URL): T? {
        try {

            var result: T? = null
            netManager.curlAndOnResponse(url) {
                result = parseObjectResponse(it)
            }

            return result
        } catch (e: Throwable) {
            throw ObjectParsingError(e,url.toString())
        }
    }

    @Throws(ObjectParsingError::class)
    fun parseObjectResponse(response: Response): T? =
            response.safeBody()?.let { body ->
                parseObjectResponse(response, body)
            }

    /**
     * Takes body as bytes to handle encoding correctly
     */
    @Throws(ObjectParsingError::class)
    abstract fun parseObjectResponse(response: Response, body: ByteArray): T?


    @Throws(ObjectParsingError::class)
    internal fun parseRssAtomBytes(baseUrl: URL, feedXml: ByteArray): T {
        try {
            feedXml.inputStream().use { return parseFeedInputStream(baseUrl, it) }
        } catch (e: NumberFormatException) {
            try {
                // Try to work around bug in Rome
                var encoding: String? = null
                val xml: String = slashPattern.replace(
                        feedXml.inputStream().use { byteArrayInputStream ->
                            XmlReader(byteArrayInputStream).use {
                                encoding = it.encoding
                                it.readText()
                            }
                        }, "")

                xml.byteInputStream(Charset.forName(encoding
                        ?: "UTF-8")).use {
                    return parseFeedInputStream(baseUrl, it)
                }
            }catch (e: Throwable) {
                throw ObjectParsingError(e,baseUrl.toString())
            }
        }catch(e: ParsingFeedException) {
            try {
                Timber.d("parser FeedException then filter error xml char..")
                // Try to clear error Xml char
                var encoding: String? = null
                val xml: String =
                    errorXmlCharPattern.replace(feedXml.inputStream().use { byteArrayInputStream ->
                    XmlReader(byteArrayInputStream).use {
                        encoding = it.encoding
                        it.readText()
                    }
                }, "")
                xml.byteInputStream(Charset.forName(encoding?: "UTF-8")).use {
                    return parseFeedInputStream(baseUrl, it)
                }
            } catch (e: Throwable) {
                throw ObjectParsingError(e, baseUrl.toString())
            }
        }
    }

    @Throws(ObjectParsingError::class)
    abstract fun parseFeedInputStream(baseUrl: URL, `is`: InputStream): T

    suspend fun getResponse(url: URL, forceNetwork: Boolean,connectTimeoutSecs: Long = DEFAULT_CONNECT_TIMEOUT_SECS): Response {
       return netManager.getResponse(url,forceNetwork,connectTimeoutSecs)
    }

    class ObjectParsingError(e: Throwable,url:String="") : Exception(url,e)
}


fun Response.safeBody(): ByteArray? {
    return this.body?.use { body ->
        if (header("Transfer-Encoding") == "chunked") {
            val source =
                    if (header("Content-Encoding") == "gzip") {
                        GzipSource(body.source())
                    } else {
                        body.source()
                    }
            val buffer = Buffer()
            try {
                var readBytes: Long = 0
                while (readBytes != -1L) {
                    readBytes = source.read(buffer, Long.MAX_VALUE)
                }
            } catch (e: EOFException) {
                // This is not always fatal - sometimes the server might have sent the wrong
                // content-length (I suspect)
                Timber.e("FeedParser Encountered EOF exception while parsing response with headers: $headers"
                )
            }
            buffer.readByteArray()
        } else {
            body.bytes()
        }
    }
}
